/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.console;

import java.io.File;
import java.io.InputStream;
import java.io.PrintStream;
import java.lang.management.ManagementFactory;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;


/**
 * Main class that can bootstrap an ActiveMQ broker console. Handles command line argument parsing to set up and run
 * broker tasks.
 */
public class Main
{

	public static final String TASK_DEFAULT_CLASS = "org.apache.activemq.console.command.ShellCommand";
	private static boolean useDefExt = true;

	private File activeMQHome;
	private File activeMQBase;
	private ClassLoader classLoader;
	private final Set<File> extensions = new LinkedHashSet<File>();
	private final Set<File> activeMQClassPath = new LinkedHashSet<File>();

	public static void main(final String[] args)
	{
		System.out.println("1");
		// Create the tmpdir if it does not exist yet..
		final File tmpdir = new File(System.getProperty("java.io.tmpdir"));
		if (!tmpdir.exists())
		{
			tmpdir.mkdirs();
		}

		final Main app = new Main();

		// Convert arguments to collection for easier management
		final List<String> tokens = new LinkedList<String>(Arrays.asList(args));
		// Parse for extension directory option
		app.parseExtensions(tokens);

		// lets add the conf directory first, to find the log4j.properties just in case its not
		// in the activemq.classpath system property or some jar incorrectly includes one
		final File confDir = app.getActiveMQConfig();
		app.addClassPath(confDir);

		// Add the following to the classpath:
		//
		// ${activemq.base}/conf
		// ${activemq.base}/lib/* (only if activemq.base != activemq.home)
		// ${activemq.home}/lib/*
		// ${activemq.base}/lib/optional/* (only if activemq.base !=
		// activemq.home)
		// ${activemq.home}/lib/optional/*
		// ${activemq.base}/lib/web/* (only if activemq.base != activemq.home)
		// ${activemq.home}/lib/web/*
		//
		if (useDefExt && app.canUseExtdir())
		{

			final boolean baseIsHome = app.getActiveMQBase().equals(app.getActiveMQHome());

			final File baseLibDir = new File(app.getActiveMQBase(), "lib");
			final File homeLibDir = new File(app.getActiveMQHome(), "lib");

			if (!baseIsHome)
			{
				app.addExtensionDirectory(baseLibDir);
			}
			app.addExtensionDirectory(homeLibDir);

			if (!baseIsHome)
			{
				app.addExtensionDirectory(new File(baseLibDir, "camel"));
				app.addExtensionDirectory(new File(baseLibDir, "optional"));
				app.addExtensionDirectory(new File(baseLibDir, "web"));
				app.addExtensionDirectory(new File(baseLibDir, "extra"));
			}
			app.addExtensionDirectory(new File(homeLibDir, "camel"));
			app.addExtensionDirectory(new File(homeLibDir, "optional"));
			app.addExtensionDirectory(new File(homeLibDir, "web"));
			app.addExtensionDirectory(new File(homeLibDir, "extra"));
		}

		// Add any custom classpath specified from the system property
		// activemq.classpath
		app.addClassPathList(System.getProperty("activemq.classpath"));

		try
		{
			final int ret = app.runTaskClass(tokens);
			System.exit(ret);
		}
		catch (final ClassNotFoundException e)
		{
			System.out.println("Could not load class: " + e.getMessage());
			try
			{
				final ClassLoader cl = app.getClassLoader();
				if (cl != null)
				{
					System.out.println("Class loader setup: ");
					printClassLoaderTree(cl);
				}
			}
			catch (final MalformedURLException e1)
			{
			}
			System.exit(1);
		}
		catch (final Throwable e)
		{
			System.out.println("Failed to execute main task. Reason: " + e);
			System.exit(1);
		}
	}

	/**
	 * Print out what's in the classloader tree being used.
	 *
	 * @param cl
	 * @return depth
	 */
	private static int printClassLoaderTree(final ClassLoader cl)
	{
		int depth = 0;
		if (cl.getParent() != null)
		{
			depth = printClassLoaderTree(cl.getParent()) + 1;
		}

		final StringBuffer indent = new StringBuffer();
		for (int i = 0; i < depth; i++)
		{
			indent.append("  ");
		}

		if (cl instanceof URLClassLoader)
		{
			final URLClassLoader ucl = (URLClassLoader) cl;
			System.out.println(indent + cl.getClass().getName() + " {");
			final URL[] urls = ucl.getURLs();
			for (int i = 0; i < urls.length; i++)
			{
				System.out.println(indent + "  " + urls[i]);
			}
			System.out.println(indent + "}");
		}
		else
		{
			System.out.println(indent + cl.getClass().getName());
		}
		return depth;
	}

	public void parseExtensions(final List<String> tokens)
	{
		if (tokens.isEmpty())
		{
			return;
		}

		int count = tokens.size();
		int i = 0;

		// Parse for all --extdir and --noDefExt options
		while (i < count)
		{
			final String token = tokens.get(i);
			// If token is an extension dir option
			if (token.equals("--extdir"))
			{
				// Process token
				count--;
				tokens.remove(i);

				// If no extension directory is specified, or next token is
				// another option
				if (i >= count || tokens.get(i).startsWith("-"))
				{
					System.out.println("Extension directory not specified.");
					System.out.println("Ignoring extension directory option.");
					continue;
				}

				// Process extension dir token
				count--;
				final File extDir = new File(tokens.remove(i));

				if (!canUseExtdir())
				{
					System.out.println("Extension directory feature not available due to the system classpath being able to load: "
							+ TASK_DEFAULT_CLASS);
					System.out.println("Ignoring extension directory option.");
					continue;
				}

				if (!extDir.isDirectory())
				{
					System.out.println("Extension directory specified is not valid directory: " + extDir);
					System.out.println("Ignoring extension directory option.");
					continue;
				}

				addExtensionDirectory(extDir);
			}
			else if (token.equals("--noDefExt"))
			{ // If token is
			  // --noDefExt option
				count--;
				tokens.remove(i);
				useDefExt = false;
			}
			else
			{
				i++;
			}
		}

	}

	public int runTaskClass(final List<String> tokens) throws Throwable
	{
		System.out.println("2");
		StringBuilder buffer = new StringBuilder();
		buffer.append(System.getProperty("java.vendor"));
		buffer.append(" ");
		buffer.append(System.getProperty("java.version"));
		buffer.append(" ");
		buffer.append(System.getProperty("java.home"));
		System.out.println("Java Runtime: " + buffer.toString());

		buffer = new StringBuilder();
		buffer.append("current=");
		buffer.append(Runtime.getRuntime().totalMemory() / 1024L);
		buffer.append("k  free=");
		buffer.append(Runtime.getRuntime().freeMemory() / 1024L);
		buffer.append("k  max=");
		buffer.append(Runtime.getRuntime().maxMemory() / 1024L);
		buffer.append("k");
		System.out.println("  Heap sizes: " + buffer.toString());

		final List<?> jvmArgs = ManagementFactory.getRuntimeMXBean().getInputArguments();
		buffer = new StringBuilder();
		for (final Object arg : jvmArgs)
		{
			buffer.append(" ").append(arg);
		}
		System.out.println("    JVM args:" + buffer.toString());
		System.out.println("Extensions classpath:\n  " + getExtensionDirForLogging());

		System.out.println("ACTIVEMQ_HOME: " + getActiveMQHome());
		System.out.println("ACTIVEMQ_BASE: " + getActiveMQBase());
		System.out.println("ACTIVEMQ_CONF: " + getActiveMQConfig());
		System.out.println("ACTIVEMQ_DATA: " + getActiveMQDataDir());

		final ClassLoader cl = getClassLoader();
		Thread.currentThread().setContextClassLoader(cl);
		System.out.println("3");
		// Use reflection to run the task.
		try
		{
			final String[] args = tokens.toArray(new String[tokens.size()]);
			final Class<?> task = cl.loadClass(TASK_DEFAULT_CLASS);
			final Method runTask = task.getMethod("main", new Class[]
			{ String[].class, InputStream.class, PrintStream.class });
			return (int) runTask.invoke(task.newInstance(), args, System.in, System.out);
		}
		catch (final InvocationTargetException e)
		{
			throw e.getCause();
		}
	}

	public void addExtensionDirectory(final File directory)
	{
		extensions.add(directory);
	}

	public void addClassPathList(final String fileList)
	{
		if (fileList != null && fileList.length() > 0)
		{
			final StringTokenizer tokenizer = new StringTokenizer(fileList, File.pathSeparator);
			while (tokenizer.hasMoreTokens())
			{
				addClassPath(new File(tokenizer.nextToken()));
			}
		}
	}

	public void addClassPath(final File classpath)
	{
		activeMQClassPath.add(classpath);
	}

	/**
	 * The extension directory feature will not work if the broker factory is already in the classpath since we have to
	 * load him from a child ClassLoader we build for it to work correctly.
	 *
	 * @return true, if extension dir can be used. false otherwise.
	 */
	public boolean canUseExtdir()
	{
		try
		{
			Main.class.getClassLoader().loadClass(TASK_DEFAULT_CLASS);
			return false;
		}
		catch (final ClassNotFoundException e)
		{
			return true;
		}
	}

	public ClassLoader getClassLoader() throws MalformedURLException
	{
		if (classLoader == null)
		{
			// Setup the ClassLoader
			classLoader = Main.class.getClassLoader();
			if (!extensions.isEmpty() || !activeMQClassPath.isEmpty())
			{

				final ArrayList<URL> urls = new ArrayList<URL>();

				for (final Iterator<File> iter = activeMQClassPath.iterator(); iter.hasNext();)
				{
					final File dir = iter.next();
					urls.add(dir.toURI().toURL());
				}

				for (final Iterator<File> iter = extensions.iterator(); iter.hasNext();)
				{
					final File dir = iter.next();
					if (dir.isDirectory())
					{
						final File[] files = dir.listFiles();
						if (files != null)
						{

							// Sort the jars so that classpath built is consistently in the same
							// order. Also allows us to use jar names to control classpath order.
							Arrays.sort(files, new Comparator<File>()
							{
								@Override
								public int compare(final File f1, final File f2)
								{
									return f1.getName().compareTo(f2.getName());
								}
							});

							for (int j = 0; j < files.length; j++)
							{
								if (files[j].getName().endsWith(".zip") || files[j].getName().endsWith(".jar"))
								{
									urls.add(files[j].toURI().toURL());
								}
							}
						}
					}
				}

				final URL u[] = new URL[urls.size()];
				urls.toArray(u);
				classLoader = new URLClassLoader(u, classLoader);
			}
			Thread.currentThread().setContextClassLoader(classLoader);
		}
		return classLoader;
	}

	public void setActiveMQHome(final File activeMQHome)
	{
		this.activeMQHome = activeMQHome;
	}

	public File getActiveMQHome()
	{
		if (activeMQHome == null)
		{
			if (System.getProperty("activemq.home") != null)
			{
				activeMQHome = new File(System.getProperty("activemq.home"));
			}

			if (activeMQHome == null)
			{
				// guess from the location of the jar
				URL url = Main.class.getClassLoader().getResource("org/apache/activemq/console/Main.class");
				if (url != null)
				{
					try
					{
						final JarURLConnection jarConnection = (JarURLConnection) url.openConnection();
						url = jarConnection.getJarFileURL();
						final URI baseURI = new URI(url.toString()).resolve("..");
						activeMQHome = new File(baseURI).getCanonicalFile();
						System.setProperty("activemq.home", activeMQHome.getAbsolutePath());
					}
					catch (final Exception ignored)
					{
					}
				}
			}

			if (activeMQHome == null)
			{
				activeMQHome = new File("../.");
				System.setProperty("activemq.home", activeMQHome.getAbsolutePath());
			}
		}

		return activeMQHome;
	}

	public File getActiveMQBase()
	{
		if (activeMQBase == null)
		{
			if (System.getProperty("activemq.base") != null)
			{
				activeMQBase = new File(System.getProperty("activemq.base"));
			}

			if (activeMQBase == null)
			{
				activeMQBase = getActiveMQHome();
				System.setProperty("activemq.base", activeMQBase.getAbsolutePath());
			}
		}

		return activeMQBase;
	}

	public File getActiveMQConfig()
	{
		File activeMQConfig = null;

		if (System.getProperty("activemq.conf") != null)
		{
			activeMQConfig = new File(System.getProperty("activemq.conf"));
		}
		else
		{
			activeMQConfig = new File(getActiveMQBase() + "/conf");
			System.setProperty("activemq.conf", activeMQConfig.getAbsolutePath());
		}
		return activeMQConfig;
	}

	public File getActiveMQDataDir()
	{
		File activeMQDataDir = null;

		if (System.getProperty("activemq.data") != null)
		{
			activeMQDataDir = new File(System.getProperty("activemq.data"));
		}
		else
		{
			activeMQDataDir = new File(getActiveMQBase() + "/data");
			System.setProperty("activemq.data", activeMQDataDir.getAbsolutePath());
		}
		return activeMQDataDir;
	}

	public String getExtensionDirForLogging()
	{
		final StringBuilder sb = new StringBuilder("[");
		for (final Iterator<File> it = extensions.iterator(); it.hasNext();)
		{
			final File file = it.next();
			sb.append(file.getPath());
			if (it.hasNext())
			{
				sb.append(",");
			}
		}
		sb.append("]");
		return sb.toString();
	}
}
